/**
 * 
 */
package org.swing.utility.jai.util;

import java.awt.image.renderable.ParameterBlock;
import java.io.*;
import javax.media.jai.*;
import com.sun.media.jai.codec.*;
import javax.media.jai.widget.*;
import java.awt.RenderingHints;
import java.awt.color.ColorSpace;
import java.awt.image.*;
import java.awt.Transparency;

/**
 * @author Quynh Nhu
 * 
 */
public class ProcessImageJai {
	protected RenderedOp the_image;
	protected int hauteur;
	protected int largeur;

	public ProcessImageJai() {
		the_image = null;
	}

	// ------------------------------------------------------------------------
	/**
	 * Constructeur charge une image à partir d'un nom de fichier et remplit les
	 * attributs
	 * 
	 * @param fileName
	 *            nom du fichier image à partir duquel l'objet est construit
	 */
	public ProcessImageJai(String fileName) {
		the_image = (RenderedOp) JAI.create("fileload", fileName);
		/* Récupérer la hauteur et la largeur de l'image. */
		largeur = the_image.getWidth();
		hauteur = the_image.getHeight();
	}

	// ------------------------------------------------------------------------
	/**
	 * Constructeur charge une image à partir d'un autre RenderedOp
	 * 
	 * @param ro
	 *            RenderedOp à partir duquel l'image est construite
	 */
	public ProcessImageJai(RenderedOp ro) {
		the_image = ro;
		/* Récupérer la hauteur et la largeur de l'image. */
		largeur = the_image.getWidth();
		hauteur = the_image.getHeight();
	}

	// ------------------------------------------------------------------------
	/**
	 * Retourne le RenderedOp contenu dans l'image
	 */
	public RenderedOp getTheImage() {
		return the_image;
	}

	// ------------------------------------------------------------------------
	/**
	 * @return la hauteur de l'image
	 */
	public int getHauteur() {
		return hauteur;
	}

	// ------------------------------------------------------------------------
	/**
	 * @return la largeur de l'image
	 */
	public int getLargeur() {
		return largeur;
	}

	// ------------------------------------------------------------------------
	/**
	 * Sauvegarde d'une image au format JPEG compressé à 75%.
	 * 
	 * @param fichier
	 *            le nom du fichier
	 */
	public void ecrireJPEG(String fichier) throws FileNotFoundException,
			IOException {
		OutputStream os = new FileOutputStream(fichier);
		JPEGEncodeParam param = new JPEGEncodeParam();
		ImageEncoder enc = ImageCodec.createImageEncoder("JPEG", os, param);
		enc.encode(the_image);
		os.close();
	}

	// ------------------------------------------------------------------------
	/**
	 * Sauvegarde d'une image au format BMP.
	 * 
	 * @param fichier
	 *            le nom du fichier
	 */
	public void ecrireBMP(String fichier) throws FileNotFoundException,
			IOException {
		OutputStream os = new FileOutputStream(fichier);
		BMPEncodeParam param = new BMPEncodeParam();
		ImageEncoder enc = ImageCodec.createImageEncoder("BMP", os, param);
		enc.encode(the_image);
		os.close();
	}

	// ------------------------------------------------------------------------
	/**
	 * Sauvegarde d'une image au format PNG.
	 * 
	 * @param fichier
	 *            le nom du fichier
	 */
	public void ecrirePNG(String fichier) throws FileNotFoundException,
			IOException {
		OutputStream os = new FileOutputStream(fichier);
		PNGEncodeParam.RGB param = new PNGEncodeParam.RGB();
		ImageEncoder enc = ImageCodec.createImageEncoder("PNG", os, param);
		enc.encode(the_image);
		os.close();
	}

	// ------------------------------------------------------------------------
	/**
	 * Sauvegarde d'une image au format TIFF.
	 * 
	 * @param fichier
	 *            le nom du fichier
	 */
	public void ecrireTIFF(String fichier) throws FileNotFoundException,
			IOException {
		OutputStream os = new FileOutputStream(fichier);
		TIFFEncodeParam param = new TIFFEncodeParam();
		ImageEncoder enc = ImageCodec.createImageEncoder("TIFF", os, param);
		enc.encode(the_image);
		os.close();
	}

	// ------------------------------------------------------------------------
	/**
	 * Sauvegarde d'une image correspondant au type MIME indiqué
	 * 
	 * @param fichier
	 *            le nom du fichier
	 * @param mime
	 *            type MIME associé au fichier
	 */
	public void ecrireImage(String fichier, String mime)
			throws FileNotFoundException, IOException {
		if (mime.equals("image/jpeg"))
			ecrireJPEG(fichier);
		else if (mime.equals("image/bmp"))
			ecrireBMP(fichier);
		else if (mime.equals("image/png"))
			ecrirePNG(fichier);
		else if (mime.equals("image/tiff"))
			ecrireTIFF(fichier);
	}

	// ------------------------------------------------------------------------
	/**
	 * Renvoie sur un flux de sortie une image stockée en mémoire. Cette méthode
	 * est particulièrement utile dans le cas d'un affichage de l'image sur le
	 * navigateur du client. (format JPEG)
	 * 
	 * @param os
	 *            flux de sortie
	 */
	public void afficheBufferedImage(OutputStream os) throws IOException {
		JPEGEncodeParam param = new JPEGEncodeParam();
		ImageEncoder enc = ImageCodec.createImageEncoder("JPEG", os, param);
		enc.encode(the_image);
		os.flush();
	}

	// ------------------------------------------------------------------------
	/**
	 * Renvoie sur un flux de sortie une image stockée en mémoire. Cette méthode
	 * est particulièrement utile dans le cas d'un affichage de l'image sur le
	 * navigateur du client.
	 * 
	 * @param os
	 *            flux de sortie
	 * @param format
	 *            format de sortie souhaité
	 */
	public void afficheBufferedImage(OutputStream os, String format)
			throws IOException {
		if (format != null) {
			ImageEncoder enc = null;
			if (format.equals("JPEG")) {
				JPEGEncodeParam param = new JPEGEncodeParam();
				enc = ImageCodec.createImageEncoder("JPEG", os, param);
			} else if (format.equals("PNG")) {
				PNGEncodeParam.RGB param = new PNGEncodeParam.RGB();
				enc = ImageCodec.createImageEncoder("PNG", os, param);
			} else if (format.equals("BMP")) {
				BMPEncodeParam param = new BMPEncodeParam();
				enc = ImageCodec.createImageEncoder("BMP", os, param);
			} else if (format.equals("TIFF")) {
				TIFFEncodeParam param = new TIFFEncodeParam();
				enc = ImageCodec.createImageEncoder("TIFF", os, param);
			} else {
				throw new IOException("Format d'export Inconnu");
			}
			enc.encode(the_image);
			os.flush();
		}
	}

	// ------------------------------------------------------------------------
	/**
	 * Redimensionne l'image et lui applique la hauteur donnee en parametre
	 * 
	 * @param hauteur2
	 *            la hauteur souhaitée de la nouvelle image
	 * @return l'image modifiee
	 */
	public ProcessImageJai redimensionneImage(int hauteur2) {
		/*
		 * On crée un objet de type interpolation bilinéaire standard qui sera
		 * utilisé avec l'opérateur "scale" (redimentionnement).
		 */
		Interpolation interp = Interpolation
				.getInstance(Interpolation.INTERP_BICUBIC);

		System.out.println("Image source -> Largeur : " + largeur + ",hauteur:"
				+ hauteur);
		float echelle = (float) hauteur2 / (float) hauteur;

		/**
		 * Ecriture de l'image source et les paramètres de la transformation
		 * dans un ParameterBlock qui servira à l'opération graphique
		 */
		ParameterBlock params = new ParameterBlock();
		params.addSource(the_image);
		params.add(echelle); // echelle sur l'axe des x
		params.add(echelle); // echelle sur l'axe des y
		params.add(0.0F); // translation en x
		params.add(0.0F); // translation en y
		params.add(interp); // méthode d'interpolation

		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		// Crée un opérateur pour mettre à l'échelle l'image1.
		ProcessImageJai the_new_image = new ProcessImageJai(JAI.create("scale",
				params, rh));
		// Renvoi de l'image modifiée
		return the_new_image;
	}

	// ------------------------------------------------------------------------
	/**
	 * Redimensionne l'image et lui applique les dimensions desirees
	 * 
	 * @param height
	 *            nouvelle hauteur
	 * @param width
	 *            nouvelle largeur
	 * @return l'image modifiee
	 */
	public ProcessImageJai redimensionneImage(int height, int width) {

		// création d'un objet de type interpolation bilinéaire standard
		// utilisé avec l'opérateur "scale" (redimensionnenement)
		Interpolation interp = Interpolation
				.getInstance(Interpolation.INTERP_BICUBIC);

		// mise à l'échelle
		float echelleH = (float) height / (float) hauteur;
		float echelleW = (float) width / (float) largeur;

		// écriture de l'image source et les paramètres de la transformation
		// dans un ParameterBlock qui servira à l'opération graphique
		ParameterBlock params = new ParameterBlock();
		params.addSource(the_image);
		params.add(echelleW); // echelle sur l'axe des x
		params.add(echelleH); // echelle sur l'axe des y
		params.add(0.0F); // translation en x
		params.add(0.0F); // translation en y
		params.add(interp); // méthode d'interpolation

		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		// Operation de redimensionnement
		ProcessImageJai the_new_image = new ProcessImageJai(JAI.create("scale",
				params, rh));

		// renvoi de la nouvelle image
		return the_new_image;
	}

	// ------------------------------------------------------------------------
	/**
	 * Récupère un morceau de l'image
	 * 
	 * @param x
	 *            debut de la selection (Ox)
	 * @param y
	 *            debut de la selection (Oy)
	 * @param largeur
	 *            largeur de la selection
	 * @param hauteur
	 *            hauteur de la selection
	 * @return l'image modifiee
	 */
	public ProcessImageJai recadreImage(int x, int y, int largeur, int hauteur) {

		// écriture de l'image source et les paramètres de la transformation
		// dans un ParameterBlock qui servira à l'opération graphique
		ParameterBlock params = new ParameterBlock();
		params.addSource(the_image);
		params.add((float) x); // Début du rectangle de sélection (x)
		params.add((float) y); // Début du rectangle de sélection (y)
		params.add((float) largeur); // Largeur de la sélection
		params.add((float) hauteur); // Hauteur de la sélection

		// Operation de recadrage
		ProcessImageJai the_new_image = new ProcessImageJai(JAI.create("crop",
				params, null));

		// renvoi de la nouvelle image
		return the_new_image;
	}

	// ------------------------------------------------------------------------
	// OPERATIONS DE FILTRAGE
	// ------------------------------------------------------------------------
	/**
	 * Filtre des couleurs de l'image initiale
	 * 
	 * @param matrix
	 *            matrice indiquant les bandes de couleurs à garder
	 * @return l'image modifiée
	 */
	private ProcessImageJai filtre(double[][] matrix) {
		if (the_image.getSampleModel().getNumBands() != 3) {
			throw new IllegalArgumentException("Image # bands <> 3");
		}
		// Creation d'un nouveau bloc de parametres
		ParameterBlock pb = new ParameterBlock();

		// Configuration du blocs de parametres necessaires à la transformation
		pb.addSource(the_image);
		pb.add(matrix);

		// Renvoi de l'image filtrée
		return new ProcessImageJai(JAI.create("bandcombine", pb, null));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de recuperer une image en niveaux de gris
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreGris() {
		// Importance relative des couleurs pour l'oeil humain
		double[][] matrix = { { 0.114D, 0.587D, 0.299D, 0.0D } };
		return filtre(matrix);
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de recuperer une image en noir et blanc
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreNB() {
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(the_image);
		ColorSpace cs = ColorSpace.getInstance(ColorSpace.CS_GRAY);
		pb.add(cs);
		return new ProcessImageJai(JAI.create("ColorConvert", pb));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de retenir la composante rouge de l'image initiale
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreR() {
		double[][] matrix = { { 1.0D, 0.0D, 0.0D, 0.0D } };
		return filtre(matrix);
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de retenir la composante verte de l'image initiale
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreV() {
		double[][] matrix = { { 0.0D, 1.0D, 0.0D, 0.0D } };
		return filtre(matrix);
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de retenir la composante bleue de l'image initiale
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreB() {
		double[][] matrix = { { 0.0D, 0.0D, 1.0D, 0.0D } };
		return filtre(matrix);
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant d'ajouter du flou à une image
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreFlou() {

		float[] blurData = { 0.0F, 1.0F / 8.0F, 0.0F, 1.0F / 8.0F, 4.0F / 8.0F,
				1.0F / 8.0F, 0.0F, 1.0F / 8.0F, 0.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, blurData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant d'ajouter plus de flou à une image
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtrePlusFlou() {
		float[] blurMoreData = { 1.0F / 14.0F, 2.0F / 14.0F, 1.0F / 14.0F,
				2.0F / 14.0F, 2.0F / 14.0F, 2.0F / 14.0F, 1.0F / 14.0F,
				2.0F / 14.0F, 1.0F / 14.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, blurMoreData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de générer du flou gaussien
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreFlouGaussien() {
		int rayon = 5;
		KernelJAI kernel = makeGaussianKernel(rayon);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de renforcer les contours (netteté)
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreRenforcement() {
		float[] kernelData = { 0.0F, -1.0F / 4.0F, 0.0F, -1.0F / 4.0F,
				8.0F / 4.0F, -1.0F / 4.0F, 0.0F, -1.0F / 4.0F, 0.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, kernelData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de renforcer davantage les contours
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtrePlusRenforcement() {
		float[] kernelData = { -1.0F / 4.0F, -1.0F / 4.0F, -1.0F / 4.0F,
				-1.0F / 4.0F, 12.0F / 4.0F, -1.0F / 4.0F, -1.0F / 4.0F,
				-1.0F / 4.0F, -1.0F / 4.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, kernelData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de renforcer davantage les contours
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreFrontieres() {
		float[] kernelData = { 0.0F, -1.0F, 0.0F, -1.0F, 4.0F, -1.0F, 0.0F,
				-1.0F, 0.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, kernelData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Filtre permettant de renforcer davantage les contours
	 * 
	 * @return l'image modifiée
	 */
	public ProcessImageJai filtreEmpreinte() {
		float[] kernelData = { -5.0F, 0.0F, 0.0F, 0.0F, 1.0F, 0.0F, 0.0F, 0.0F,
				5.0F };

		KernelJAI kernel = new KernelJAI(3, 3, 1, 1, kernelData);

		return new ProcessImageJai(convolution(kernel));
	}

	// ------------------------------------------------------------------------
	/**
	 * Opération de filtrage : convolution avec le filtre caractérisé par le
	 * noyau de la convolution
	 * 
	 * @param kernel
	 *            noyau contenant les coefficients de convolution
	 * @return le RenderedOp modifié
	 */
	private RenderedOp convolution(KernelJAI kernel) {
		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		ParameterBlock paramBlock = new ParameterBlock();
		paramBlock.addSource(the_image);
		paramBlock.add(kernel);
		return JAI.create("convolve", paramBlock, rh);
	}

	// ------------------------------------------------------------------------
	/**
	 * Création d'un noyau de convolution gaussien pour appliquer les filtres
	 * dont on a besoin
	 * 
	 * @param rayon
	 *            rayon du noyau
	 */
	private KernelJAI makeGaussianKernel(int rayon) {
		int diametre = 2 * rayon + 1;
		float inverseRayonCarre = 1.0F / (rayon * rayon);
		float[] gaussianData = new float[diametre];

		float somme = 0.0F;
		for (int i = 0; i < diametre; i++) {
			float d = i - rayon;
			float val = (float) Math.exp(-d * d * inverseRayonCarre); // gaussienne!!
			gaussianData[i] = val;
			somme += val;
		}

		// Normalisation
		float invsomme = 1.0F / somme;
		for (int i = 0; i < diametre; i++) {
			gaussianData[i] *= invsomme;
		}

		return new KernelJAI(diametre, diametre, rayon, rayon, gaussianData,
				gaussianData);
	}

	// ------------------------------------------------------------------------
	// OPERATIONS DE TRANSPOSITION (Rotations, Symétries)
	// ------------------------------------------------------------------------
	/**
	 * Rotation de l'image d'un angle donné
	 * 
	 * @param angle
	 *            angle de rotation (en degrés)
	 * @return l'image modifiée
	 */
	public ProcessImageJai rotation(int angle) {
		// Conversion de l'angle de rotation en radians
		float valeur = (float) (angle * (Math.PI / 180.0F));

		// Creation d'un bloc de paramètres + spécification de la source
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(the_image); // image source
		pb.add(0.0F); // origine en X du point de rotation
		pb.add(0.0F); // origine en Y du point de rotattion
		pb.add(valeur); // angle de rotation
		pb.add(new InterpolationNearest()); // interpolation utilisée

		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		// Renvoi de l'image modifiée
		return new ProcessImageJai(JAI.create("Rotate", pb, rh));
	}

	// ------------------------------------------------------------------------
	/**
	 * Symétrie de l'image
	 * 
	 * @param type
	 *            type de symétrie (FLIP_VERTICAL, FLIP_HORIZONTAL,
	 *            FLIP_DIAGONAL,FLIP_ANTIDIAGONAL)
	 * @return l'image modifiée
	 */
	public ProcessImageJai symetrie(int type) {
		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		// Creation d'un bloc de paramètres + spécification de la source
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(the_image);
		pb.add(type);

		return new ProcessImageJai(JAI.create("transpose", pb, rh));
	}

	// ------------------------------------------------------------------------
	// ------------------------------------------------------------------------
	/**
	 * Gestion de la balance des couleurs
	 * 
	 * @param rouge
	 *            degré du rouge dans l'image
	 * @param vert
	 *            degré de vert dans l'image
	 * @param bleu
	 *            degré de bleu dans l'image
	 * @return l'image modifiée
	 */
	public ProcessImageJai balanceCouleurs(int rouge, int vert, int bleu) {
		// Rendering Hints -> Cadre de 0 autour
		RenderingHints rh = new RenderingHints(JAI.KEY_BORDER_EXTENDER,
				BorderExtender.createInstance(BorderExtender.BORDER_ZERO));

		// Creation d'un bloc de paramètres + spécification de la source
		ParameterBlock pb = new ParameterBlock();
		pb.addSource(the_image);
		double consts[] = { rouge, vert, bleu };
		pb.add(consts);

		// renvoi de l'image modifiée
		return new ProcessImageJai(JAI.create("addconst", pb, rh));
	}

	// ------------------------------------------------------------------------
	// ------------------------------------------------------------------------
	/**
	 * Gestion ducontraste
	 * 
	 * @param intensite
	 *            intensité du contraste
	 * @return l'image modifiée
	 */
	public ProcessImageJai contraste(int intensite) {

		ParameterBlock pb = new ParameterBlock();
		ROI roi = null; // Region à scanner (null -> toute l'image)
		pb.addSource(the_image); // Image source
		pb.add(roi); // Region de l'image à scanner
		pb.add(50); // echantillonnage horizontal
		pb.add(50); // echantillonnage vertical

		// Recherche des extremas sur l'image source
		RenderedOp op = JAI.create("extrema", pb);
		// Recupere les valeurs max et min des pixels de l'image
		double[][] extrema = (double[][]) op.getProperty("extrema");

		int bands = extrema[0].length;
		double[] map = new double[bands];

		for (int i = 0; i < bands; i++) {
			map[i] = (double) intensite;
		}

		// Create the threshold operation.
		pb = new ParameterBlock();
		pb.addSource(the_image);
		pb.add(extrema[1]); // Minimums
		pb.add(extrema[0]); // Maximums
		pb.add(map);

		// renvoi de l'image modifiée
		return new ProcessImageJai(JAI.create("threshold", pb));
	}
}
